/*
 * psh.go
 * Telnet Honeypot
 * by J. Stuart McMurray
 * Created 20131029
 * Last Modified 20131123
 */

package main

import (
        "crypto/rand"
        "bufio"
        "fmt"
        "io"
        "io/ioutil"
        "log"
        "math/big"
        "net"
        "os"
        "path/filepath"
        "strconv"
        "strings"
        "sync"
        "time"
)

/*************
 * Constants *
 *************/
 /* TODO: un-hardcode these */
var usernamePrompt []byte = []byte("Username: ")
var passwordPrompt []byte = []byte("Password: ")

/********************
 * Global Variables *
 ********************/
/* TODO: Trap ctrl+C, kippo style. */
/* stdin as an io.Reader */
var stdin *bufio.Reader
/* Global config map */
var config = map[string]string{
        "motdfile":      "-",
        "motd":          "Someone didn't edit the psh config...",
        "logfile":       "-",
        "sessionlogdir": "-",
        "listenaddr":    "-",
        "logtostderr":   "true",
        "prompt":        "[root@pooh:~]#",
        "readtimeout":   "60",
}
/* Log directory for Sessions */
var sessionLogDir string
/* Mutex to keep from clobbering log files in session dir */
var sessionLogDirMutex sync.Mutex
/* Read Timeout for established connections */
var readTimeout int
/* MOTD as bytes */
var motdBytes []byte

/* TODO: Note in the config file what takes precedence */
/*************************
 * Structure Definitions *
 *************************/
/* Struct for use for logging */
type multiLogger struct {
        Stderr      io.Writer
        GlobalFile  io.Writer
        SessionFile io.Writer
}
/* Implement io.Writer */
func (m multiLogger) Write(p []byte) (n int, err error) {
        n = 0
        for _,w := range [3]io.Writer{m.Stderr, m.GlobalFile, m.SessionFile} {
                if w != nil {
                        c, e := w.Write(p)
                        n += c
                        if e != nil {
                                return
                        }
                }
        }
        return
}

/* Off we go */
func main() {
        /* Parse the config file */
        parseConfig()

        /* Set up logging */
        log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
        logDestinations := multiLogger{}

        /* Open a logfile if necessary */
        if config["logfile"] != "-" {
                logfile, err := os.OpenFile(config["logfname"],
                    os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
                defer logfile.Close()
                if err != nil {
                        log.Fatalf("Unable to open log file %v: %v",
                            config["logfname"], err)
                }
                logDestinations.GlobalFile = logfile
        }
        /* Decide whether or not to log to stderr */
        if config["logtostderr"] != "-" {
                logDestinations.Stderr = os.Stderr
        }
        /* Tell the logger to use what we've got */
        log.SetOutput(logDestinations)

        /* Make the directory to use if we're logging sessions */
        if config["sessionlogdir"] != "-" {
                sessionLogDir = filepath.FromSlash(config["sessionlogdir"])
                /* Make the directory if it doesn't exist */
                if err := os.MkdirAll(sessionLogDir, 0644); err != nil {
                        log.Printf("Unable to make %v: %v", sessionLogDir, err)
                        return
                }

        }

        /* Come up wth a listen port if need be */
        if config["listenaddr"] == "-" {
                portnum, err := rand.Int(rand.Reader, big.NewInt(55534))
                if err != nil {
                        log.Fatalf("Unable to generate random port: %v", err)
                }
                config["listenaddr"] = fmt.Sprintf(":%v",
                    portnum.Int64() + 10000)
                    /* TODO: Implement real telnet protocol messages */
        }

        /* Work out what the MOTD is */
        if config["motdfile"] != "-" {
                motd, err := ioutil.ReadFile(config["motdfile"])
                if err != nil {
                        log.Printf("Unable to read MOTD from %v: %v",
                            config["motdfile"], err)
                } else {
                        config["motd"] = string(motd)
                }
        }
        /* Make sure it has a newline */
        if !strings.HasSuffix(config["motd"], "\n") {
                config["motd"] += "\n"
        }
        motdBytes = []byte(config["motd"])

        /* Handle Prompt */
        config["prompt"] = strings.TrimSpace(config["prompt"])
        config["prompt"] += " "

        /* Convert read timeout to a number */
        if r, err := strconv.Atoi(config["readtimeout"]); err != nil {
                log.Fatalf("Unable to convert %v to a number: %v",
                    config["readtimeout"], err)
        } else {
                readTimeout = r
        }

        /* Work out the address on which to listen */
        listenAddr, err := net.ResolveTCPAddr("tcp", config["listenaddr"])
        if err != nil {
                log.Fatalf("Unable to understand %v: %v",
                    config["listenaddr"], err)
        }
        /* Listen for connections */
        listenSock, err := net.ListenTCP("tcp", listenAddr)
        /* Come up with the prefix for the listener */
        /* Log it or Die */
        if err != nil {
                log.Fatalf("Unable to listen on %v: %v", listenAddr, err)
        } else {
                log.Printf("Listening on %v.", listenSock.Addr())
        }

        /* Main program loop */
        sessionCount := 0
        for {
                /* Wait for incoming connections */
                conn, err := listenSock.AcceptTCP()
                if err != nil {
                        log.Fatalf("Error while accepting client: %v", err)
                }
                /* Handle each connection */
                sessionCount++
                go handleConn(conn, sessionCount, logDestinations)
        }
}


/* Goroutine to handle incoming connection */
func handleConn(conn *net.TCPConn, sessionID int,
    logDestinations multiLogger) {
        /* Close the connection when we're done */
        defer conn.Close()

        /* Set up logger */
        logPrefix := fmt.Sprintf("[%v] %v ", sessionID, conn.RemoteAddr())
        l := log.New(logDestinations, logPrefix,
            log.Ldate | log.Ltime | log.Lmicroseconds)
        /* Session logfile */
        //var logFile *os.File = nil
        /* Timestamp for the connection */
        //timeStamp := time.Now().Unix()

        /* Log the connection */
        l.Printf("Accepted connection")

        /* Print MOTD */
        if _, err := sendAllData(motdBytes, conn); err != nil {
                /* If there was an error in sending */
                l.Printf("Unable to send MOTD to %v: %v",
                    conn.RemoteAddr(), err)
                return
        }

        /* Prompt for a username */
        if _, err := sendAllData(usernamePrompt, conn); err != nil {
                l.Printf("Unable to prompt for username: %v", err)
                return
        }
        /* Get username */
        username, err, _ := getLine(conn)
        /* TODO: parse cmd every getLine, if found send conn to a goroutine */
        /* With the default ubuntu/kali prompt, return */
        if err != nil {
                l.Printf("Unable to get username: %v", err)
                return
        }
        l.Printf("Username: %v", string(username))

        /* Prompt for password */
        if _, err := sendAllData(passwordPrompt, conn); err != nil {
                l.Printf("Unable to prompt for password: %v", err)
                return
        }
        /* TODO: turn off echo */
        /* Get password */
        password, err, _ := getLine(conn)
        if err != nil {
                l.Printf("Unable to get password: %v", err)
                return
        }
        l.Printf("Password: %v", string(password))

        /* TODO: Implement list of allowed usernames and passwords */

        /* TODO: start logging somewhere around here */

        for {
                /* TODO: Send real prompt */
                if _, err := sendAllData([]byte("[root@http]# "), conn);
                    err != nil {
                        l.Printf("Error when sending prompt: %v", err)
                        return
                }
                buf, err, _ := getLine(conn)
                if err != nil {
                        if oerr, ok := err.(*net.OpError); ok &&
                            oerr.Timeout() {
                                l.Printf("Timedout waiting for read")
                                break
                        } else {
                                l.Printf("Read error: %v", err)
                                break
                        }
                } else {
                        l.Printf("RX line: %v",
                            strings.TrimSpace(string(buf)))
                }
                if _, err := sendAllData([]byte("Penis.\n"), conn);
                    err != nil {
                        l.Printf("Error when sending penis: %v", err)
                        return
                }
        }


        /* Ask for username and password */
        /* TODO: Read timeout */
        /* Print Prompt */
        return


        /* Open a connection-specific logfile if desired */
        if len(sessionLogDir) > 0 {
                /* Come up with a unique name for the logfile */
                logfname := filepath.Join(sessionLogDir,
                    fmt.Sprintf("psh-%v-%v.log", sessionID, conn.RemoteAddr()))
                /* TODO: Record log files in a playable format. */
                /* Open the file */
                logFile, err := os.OpenFile(logfname,
                    os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
                if logFile != nil {
                        defer logFile.Close()
                }
                if err != nil {
                        log.Printf("Unable to open session log file %v for " +
                            "connection from %v: %v", config["logfname"],
                            conn.RemoteAddr(), err)
                }

        }
}

/* TODO: Document cmd somewhere, use it to pass interesting telnet commands */
/* such as ctrl+C */

/* Read a line to include the \n, ignoring protocol bytes and such */
func getLine(conn *net.TCPConn) (line []byte, e error, cmd []string) {
        line = make([]byte, 0)
        for {
                /* Get a byte */
                b, err := getByte(conn)
                if err != nil {
                        e = err
                        return
                }
                /* Handle protocol messages */
                if b == 0xFF {
                        next, err := getByte(conn)
                        if err != nil {
                                e = err
                                return
                        }
                        /* Handle literal 255's */
                        if next == 255 {
                                line = append(line, byte(255))
                                continue
                        ///* Got a Ctrl+C */
                        //} else if next == 244 {
                                //cmd = append(cmd, "IN")
                                //continue
                        /* Got a linefeed */
                        } else if next == 10 {
                                line = append(line, byte('\n'))
                        ///* Got a DO.  Respond with a WONT */
                        //} else if next == 253 {
                                //c, err := getByte(conn)
                                //if err != nil {
                                        //e = err
                                        //return
                                //}
                                //log.Printf("2got %v", c)
                                //sendAllData([]byte{255, 252, c}, conn)
                                ///* TODO: Make the above WON'T work */
                                
                        /* Got a suboption start */
                        } else if b == 0xFF {
                                /* Read bytes until we get FF F0 */
                                var a, b byte = 0, 0
                                for !(a == 0xFF && b == 0xF0) {
                                        a = b
                                        b, err = getByte(conn)
                                        if err != nil {
                                                e = err
                                                return
                                        }
                                }
                        /* Anything else is a 3-byte protocol message */
                        } else {
                                getByte(conn)
                                getByte(conn)
                        }
                }
                /* Handle newlines */
                if b == byte('\r') {
                        next, err := getByte(conn)
                        if err != nil {
                                e = err
                                return
                        }
                        if next == byte('\n') {
                                line = append(line, byte('\n'))
                                return
                        } else {
                                line = append(line, byte('\r'))
                        }
                } else if b == byte('\n') {
                        line = append(line, byte('\n'))
                        return
                }
                /* Append all other characters */
                line = append(line, b)
        }
}


/* Get a byte from a conn */
func getByte(conn *net.TCPConn) (b byte, err error) {
        conn.SetReadDeadline(time.Now().Add(
            time.Duration(readTimeout)*time.Second))
        buf := make([]byte, 1)
        _, err = conn.Read(buf)
        b = buf[0]
        return
}

/* Sends all the data on the conn */
func sendAllData(data []byte, conn net.Conn) (n int, err error) {
        for n = 0; n < len(data); {
                /* Number of bytes sent this time around */
                var count int = 0
                /* Try to send the data */
                count, err = conn.Write(data)
                /* Add to count sent */
                n += count
                /* Give up on error */
                if err != nil {
                        return
                }
        }
        return
}

/* Parse the config file, populate config */
func parseConfig() {
        /* Open Config File */
        cfgFile, err := os.Open("psh.conf") /* TODO: unhardcode this */
        if err != nil {
                log.Printf("Unable to open psh.conf: %v", err)
                return
        }
        /* Scanner to read lines from the file */
        scanner := bufio.NewScanner(cfgFile)
        if scanner == nil {
                log.Fatalf("Cannot make scanner for psh.conf")
        }
        /* Loop over lines in file */
        for scanner.Scan() {
                /* chomp */
                cfgLine := strings.TrimSpace(scanner.Text())
                /* Skip blank lines and comments */
                if len(cfgLine) == 0 || strings.HasPrefix(cfgLine, "#") {
                        continue
                }
                /* Split into key and value on first space */
                lineParts := strings.SplitN(cfgLine, " ", 2)
                /* Make sure it's a good line */
                if len(lineParts) < 2 {
                        log.Fatalf("Found funny line in config file: %v",
                            cfgLine)
                }
                config[lineParts[0]] = lineParts[1]
        }
        return
}

